package com.vpen.account.demo.aspect;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.MD5;
import com.vpen.account.demo.annotation.Resubmit;
import com.vpen.account.demo.handler.AccountDemoException;
import com.vpen.account.demo.utils.CodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RedissonClient;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.Duration;
import java.time.temporal.ChronoUnit;

/**
 * 描述
 *
 * @author 韦鹏
 * @date 2022/8/16 17:19
 */

@Slf4j
@Component
@Aspect
public class ResubmitAspect {
    @Pointcut("@annotation(com.vpen.account.demo.annotation.Resubmit)")
    public void resubmitPointcut() {
    }

    @Around("resubmitPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        // 获取注解
        Resubmit annotation = method.getAnnotation(Resubmit.class);

        // 如果可以重复提交，直接过
        if (!annotation.enable()) {
            joinPoint.proceed();
        }

        // 解析提交的参数用来做特定的key
        StringBuilder builder = new StringBuilder();
        for (String parse : annotation.keys()) {
            String value = parseExpression(parse, method, joinPoint.getArgs());
            builder.append(value);
        }
        // 生成唯一的key
        String key = genKey(signature.getDeclaringType().getSimpleName(), signature.getName(), annotation.prefixKey(), builder);
        // 如果已经存在key,说明已经提交过，就直接范围
        if (alreadyExist(key, annotation.seconds(), annotation.times())) {
            throw new AccountDemoException("请勿重复提交....");
        }
        return joinPoint.proceed();
    }


    /**
     * el解析参数列表
     *
     * @param expressionString 要解析的字段
     * @param method           反射的方法
     * @param args             参数列表
     * @return java.lang.String
     * @author 韦鹏
     * @date 2022/8/17 11:30
     * @see <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions">spel文档</a>
     **/
    private String parseExpression(String expressionString, Method method, Object[] args) {
        //获取被拦截方法参数名列表,就是获取形参名
        LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
        String[] paramNameArr = discoverer.getParameterNames(method);
        if (ObjectUtil.isNull(paramNameArr)) {
            return null;
        }
        // el将参数和形参对应起来
        ExpressionParser parser = new SpelExpressionParser();
        // context可以看做一个entity
        StandardEvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < paramNameArr.length; i++) {
            // 用了一个map存
            context.setVariable(paramNameArr[i], args[i]);
        }
        return parser.parseExpression(expressionString).getValue(context, String.class);
    }

    /**
     * 生成唯一的key
     *
     * @return java.lang.String
     * @author 韦鹏
     * @date 2022/8/16 17:55
     **/
    private String genKey(String className, String methodName, String prefixKey, StringBuilder buffer) {
        // 自定义的前缀 + 类名 + 方法名 + 参数值
        return prefixKey + className + methodName + MD5.create().digestHex(buffer.toString());
    }


    /**
     * 是否已经提交过
     *
     * @param key     键
     * @param seconds 过期时间
     * @param times   时间范围内允许的次数
     * @return boolean
     * @author 韦鹏
     * @date 2022/8/16 18:27
     **/
    private boolean alreadyExist(String key, int seconds, int times) {
        RedissonClient redisson = CodeUtils.getRedisson();
        RAtomicLong atomicLong = redisson.getAtomicLong(key);
        // 不存在
        if (!atomicLong.isExists()) {
            log.debug("key不存在");
            atomicLong.incrementAndGet();
            atomicLong.expire(Duration.of(seconds, ChronoUnit.SECONDS));
            return false;
        }
        // 存在,如果是否符合在次数范围内，不在，返回true，在返回false
        log.debug("key存在");
        return atomicLong.incrementAndGet() > times;
    }


}
